Obsidian EndTime 1.1.7
Obsidian End Time 1.1.8
2025/08/12
負荷軽減
コメント非表示
code:js
const { ItemView, WorkspaceLeaf } = require("obsidian");
// 日付ノート名かを判定(YYYY-MM-DD.md)
function isDailyNoteName(name) {
return /^\d{4}-\d{2}-\d{2}\.md$/.test(name || "");
}
// デバウンス(0.25秒待って1回だけ実行)
function debounce(fn, wait = 250) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); }; }
// スロットル(一定間隔内は1回だけ実行)
function throttle(fn, wait = 200) {
let last = 0;
return (...args) => {
const now = Date.now();
if (now - last >= wait) {
last = now;
fn(...args);
}
};
}
// ★ ハッシュ関数(そのまま流用可能)
function hashString(str) {
let hash = 0, i, chr;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
}
const VIEW_TYPE_TASK_SIDEBAR = "task-sidebar-view";
class TaskSidebarView extends ItemView {
constructor(leaf) {
super(leaf);
this._prevTasksJson = ""; // 前回描画内容のキャッシュ
}
getViewType() {
return VIEW_TYPE_TASK_SIDEBAR;
}
getDisplayText() {
return "Task Sidebar";
}
async onOpen() {
console.log("TaskSidebarView opened.");
const container = this.containerEl.children1; // Safely access the second child of containerEl
if (!container) {
console.error("Container not found in TaskSidebarView.");
return;
}
container.empty();
container.createEl("h4", { text: "Task Schedule" });
}
async updateTasks(taskList) {
const container = this.containerEl.children1;
if (!container) {
console.error("Container not found when updating tasks.");
return;
}
// 差分検知:同一内容なら何もしない
const nextJson = JSON.stringify(taskList || []);
if (nextJson === this._prevTasksJson) return;
this._prevTasksJson = nextJson;
// ここから従来どおり再描画
container.empty();
// まとめて挿入してレイアウトコストを下げる(おまけの最適化)
const frag = document.createDocumentFragment();
(taskList || []).forEach((task) => {
let taskName, timeInfo = String(task).split('||');
// Link(url) → Link
taskName = (taskName || "").replace(/\[(^\]+)\]\(^\)+\)/g, '$1');
const taskItem = document.createElement("div");
taskItem.addClass("task-item");
let taskNameEl = document.createElement("p");
taskNameEl.addClass(taskName.startsWith("🗂️") ? "section" : "task-name");
taskNameEl.textContent = taskName || "";
taskItem.appendChild(taskNameEl);
const timeEl = document.createElement("p");
timeEl.addClass("task-time");
timeEl.style.color = "#888";
timeEl.style.fontSize = "0.9em";
timeEl.textContent = timeInfo || "";
taskItem.appendChild(timeEl);
frag.appendChild(taskItem);
});
container.appendChild(frag);
}
async onClose() {
console.log("TaskSidebarView closed.");
}
}
module.exports = class TaskSchedulePlugin extends require("obsidian").Plugin {
async onload() {
console.log("Task Schedule Plugin loaded.");
this.registerView(VIEW_TYPE_TASK_SIDEBAR, (leaf) => new TaskSidebarView(leaf));
this.addCommand({
id: "show-tasks-sidebar",
name: "Show Tasks in Sidebar",
callback: () => this.showTasksSidebar(),
});
this.activateView();
// ★追加:前回テキストのハッシュ(差分検知)
this._prevTextHash = "";
// ★変更:リアルタイム反映をデバウンス→スロットルに(即応性UP)
this._throttledRT = throttle(() => this.showTasksSidebar(true), 200);
// 編集イベント:アクティブファイルが YYYY-MM-DD.md 以外なら一切処理しない
this.registerEvent(this.app.workspace.on("editor-change", () => {
const f = this.app.workspace.getActiveFile?.();
if (!f || !isDailyNoteName(f.name)) return; // ★ガード
this._throttledRT();
}));
// ファイル切替イベント:開いたファイルが日付ノートなら todayFile を更新して即反映
this.registerEvent(this.app.workspace.on("file-open", (file) => {
if (!file || !isDailyNoteName(file.name)) return; // ★ガード
this.todayFile = file;
this.showTasksSidebar(true); // 再チェック&即時更新
}));
}
onunload() {
console.log("Task Schedule Plugin unloaded.");
this.app.workspace.detachLeavesOfType(VIEW_TYPE_TASK_SIDEBAR);
clearInterval(this.updateInterval);
}
async activateView() {
console.log("Activating the TaskSidebarView...");
try {
let leaf = this.app.workspace.getLeavesOfType(VIEW_TYPE_TASK_SIDEBAR).first();
if (!leaf) {
console.log("No existing sidebar leaf found, creating a new one.");
const rightLeaf = this.app.workspace.getRightLeaf(false);
if (!rightLeaf) {
console.log("No right leaf found, creating a new one by splitting the workspace.");
leaf = this.app.workspace.createLeafBySplit(this.app.workspace.rootSplit, "horizontal", false);
} else {
leaf = rightLeaf;
}
}
await leaf.setViewState({
type: VIEW_TYPE_TASK_SIDEBAR,
active: true,
});
this.app.workspace.revealLeaf(leaf);
console.log("TaskSidebarView activated and revealed.");
} catch (error) {
console.error("Error while activating the TaskSidebarView:", error);
}
}
// force 引数を追加
showTasksSidebar(force = false) {
console.log("Displaying tasks in the sidebar.");
// 現在アクティブなファイルを取得
const activeFile = this.app.workspace.getActiveFile?.();
if (!activeFile || !isDailyNoteName(activeFile.name)) {
console.log("Active file is not a daily note. Skipping sidebar update.");
return;
}
const todayFile = activeFile;
const todayFileName = todayFile.name;
// showTasksSidebar 内 cachedRead 後の処理を以下に変更
this.app.vault.cachedRead(todayFile).then((noteText) => {
const lines = noteText.split(/\r?\n/);
const endTimes = [];
let currentDateTime = new Date();
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line) continue;
const norm = line.normalize('NFKC');
const checklist = norm.match(/^\s*-\s*\[( xX)\]\s*(.*)$/);
if (!checklist) continue;
const checkboxMark = checklist1;
const rest = checklist2.trim();
if (checkboxMark.toLowerCase() === 'x') continue;
if (/\b\d{2}:\d{2}\s*-–—−\s*\d{2}:\d{2}\b/.test(rest)) continue;
const m = rest.match(/^(?:(\d{2}:\d{2}))?-?\s*(?:\((\d+)\))?\s*(.*)$/);
if (!m) continue;
const startTime = (m1 || '').trim();
const durationStr = (m2 || '').trim();
const durationMin = durationStr !== '' ? (parseInt(durationStr, 10) || 0) : 0;
let taskName = (m3 || '').trim();
taskName = taskName.replace(/\[(^\]+)\]\(^\)+\)/g, "$1");
taskName = taskName.split(";")0.trim(); // ; 以降をコメントとして削除
let baseTime;
if (startTime) {
const h, mm = startTime.split(':').map(Number);
baseTime = new Date();
baseTime.setHours(h, mm, 0, 0);
} else {
baseTime = new Date(currentDateTime);
}
const actualStartTime = new Date(baseTime);
const startTimeStr = actualStartTime.toTimeString().slice(0, 5);
const endDate = new Date(baseTime.getTime() + durationMin * 60000);
if (durationMin > 0 && endDate < new Date()) {
endDate.setTime(Date.now());
}
const endTimeStr = endDate.toTimeString().slice(0, 5);
let tailNote = "";
if (startTime && durationMin > 0) {
const elapsedMin = Math.ceil((Date.now() - actualStartTime.getTime()) / 60000);
if (elapsedMin > durationMin) {
tailNote = 🚨超過${elapsedMin - durationMin}分;
} else {
tailNote = ➡️残り${durationMin - elapsedMin}分;
}
}
endTimes.push(${taskName}||${startTimeStr} - ${endTimeStr} (${durationMin}分)${tailNote});
currentDateTime = endDate;
}
// ★ タスク部分のみでハッシュ比較(改行しなくても差分検出可能)
const newHash = hashString(endTimes.join("\n"));
if (!force && this._prevTaskHash === newHash) return;
this._prevTaskHash = newHash;
const view = this.app.workspace.getLeavesOfType(VIEW_TYPE_TASK_SIDEBAR)0?.view;
if (view instanceof TaskSidebarView) {
view.updateTasks(endTimes.length ? endTimes : "(本日のタスクは見つかりませんでした)||");
}
}).catch((error) => {
console.error("Error reading the file:", error);
});
}
};